package com.xsjiong.learning;

import java.util.Arrays;
import java.util.Random;
import java.util.Scanner;
import java.io.*;

public class NeuralNetwork {
	public static final double SIGMOID_ONE = 0.731058578630005;

	public final double W[][][];
	public double Rate;
	public final double V[][], E[][];
	public ActivationFunction[] AF;

	public NeuralNetwork(DataInputStream in) throws IOException {
		Rate = in.readDouble();
		SM = in.readBoolean();
		int[] layers=new int[in.readInt()];
		AF = new ActivationFunction[layers.length - 1];
		for (int i=0;i < AF.length;i++)
			AF[i] = ActivationFunction.ORDERS[in.readInt()];
		for (int i=0;i < layers.length;i++) layers[i] = in.readInt();
		W = new double[layers.length - 1][][];
		V = new double[layers.length][];
		E = new double[layers.length][];
		W[0] = new double[layers[0]][];
		for (int i = 0; i < layers.length; i++) {
			V[i] = new double[layers[i]];
			E[i] = new double[layers[i]];
			if (i != layers.length - 1)
				W[i] = new double[layers[i] + 1][layers[i + 1]];
		}
		for (int i = 0, j, k; i < W.length; i++)
			for (j = 0; j < W[i].length; j++)
				for (k = 0; k < W[i][j].length; k++)
					W[i][j][k] = in.readDouble();
	}

	public void WriteTo(DataOutputStream out) throws IOException {
		out.writeDouble(Rate);
		out.writeBoolean(SM);
		out.writeInt(V.length);
		for (int i=0,q;i < AF.length;i++) {
			for (q = 0;q < ActivationFunction.ORDERS.length;q++)
				if (ActivationFunction.ORDERS[q] == AF[i]) break;
			out.writeInt(q);
		}
		for (int i=0;i < V.length;i++) out.writeInt(V[i].length);
		for (int i = 0, j, k; i < W.length; i++)
			for (j = 0; j < W[i].length; j++)
				for (k = 0; k < W[i][j].length; k++)
					out.writeDouble(W[i][j][k]);
	}

	public boolean SM;

	public NeuralNetwork(double learnrate, int[] layers) {
		this(learnrate, false, null, layers);
	}

	public NeuralNetwork(double learnrate, boolean softmax, ActivationFunction[] af, int[] layers) {
		if (layers.length < 2) throw new IllegalArgumentException("The Number Of The Layers Cannot Be Lower Than 2");
		// TODO Length warning of AF, should equals to layers.length-1
		this.Rate = learnrate;
		this.SM = softmax;
		AF = new ActivationFunction[af.length];
		if (af == null)
			System.arraycopy(af, 0, AF, 0, af.length);
		else
			Arrays.fill(AF, ActivationFunction.Sigmoid);
		W = new double[layers.length - 1][][];
		V = new double[layers.length][];
		E = new double[layers.length][];
		int i;
		W[0] = new double[layers[0]][];
		for (i = 0; i < layers.length; i++) {
			V[i] = new double[layers[i]];
			E[i] = new double[layers[i]];
			if (i != layers.length - 1)
				W[i] = new double[layers[i] + 1][layers[i + 1]];
		}
	}

	public void RandomizeWeight() {
		RandomizeWeight(System.currentTimeMillis());
	}

	public void RandomizeWeight(long seed) {
		Random r = new Random(seed);
		for (int i = 0, j, k; i < W.length; i++)
			for (j = 0; j < W[i].length; j++)
				for (k = 0; k < W[i][j].length; k++)
					W[i][j][k] = r.nextDouble();
	}

	public int GetLayerSize() {
		return W.length + 1;
	}

	public static interface ActivationFunction {
		double f(double a);

		double ff(double a);

		ActivationFunction Sigmoid = new ActivationFunction() {
			@Override
			public double f(double a) {
				return 1d / (Math.exp(-a) + 1d);
			}

			@Override
			public double ff(double a) {
				return a * (1d - a);
			}
		}, Tanh = new ActivationFunction() {
			@Override
			public double f(double a) {
				return (Math.exp(a) - Math.exp(-a)) / (Math.exp(a) + Math.exp(-a));
			}

			@Override
			public double ff(double a) {
				return 1 - a * a;
			}
		}, ReLU = new ActivationFunction() {
			@Override
			public double f(double a) {
				return Math.max(0, a);
			}

			@Override
			public double ff(double a) {
				return a >= 0 ? 1 : 0;
			}
		}, None = new ActivationFunction() {
			@Override
			public double f(double a) {
				return a;
			}

			@Override
			public double ff(double a) {
				return 1;
			}
		}, LeakyReLU = new ActivationFunction() {
			public static final double LEACKAGE = 0.01;

			@Override
			public double f(double a) {
				return (a >= 0) ? a : (a * LEACKAGE);
			}

			@Override
			public double ff(double a) {
				return a >= 0 ? 1 : LEACKAGE;
			}
		};

		public static final ActivationFunction[] ORDERS={
			Sigmoid,Tanh,ReLU,None,LeakyReLU
		};
	}

	public int GetInputSize() {
		return V[0].length;
	}

	public int GetOutputSize() {
		return V[V.length - 1].length;
	}

	public void Input(double[] input) {
		if (input.length != GetInputSize())
			throw new IllegalArgumentException("Expected Input Size: " + GetInputSize());
		System.arraycopy(input, 0, V[0], 0, input.length);
		double sum;
		int x,i,j;
		for (x = 0; x < V.length - 1; x++)
			for (j = 0; j < V[x + 1].length; j++) {
				for (i = 0, sum = W[x][V[x].length][j]; i < V[x].length; i++)
					sum += V[x][i] * W[x][i][j];
				V[x + 1][j] = AF[x].f(sum);
			}
		if (SM) {
			sum = 0;
			double[] output=GetOutput();
			double max=MaxValue();
			for (i = 0;i < output.length;i++) sum += (output[i] = Math.exp(output[i] - max));
			//System.out.println("SUM: " + sum);
			for (i = 0;i < output.length;i++) output[i] /= sum;
		}
	}

	public double[] GetOutput() {
		return V[V.length - 1];
	}

	public void Fit(double[] output) {
		if (output.length != GetOutputSize())
			throw new IllegalArgumentException("Expected Output Size: " + GetOutputSize());
		int smi=0;
		if (SM) {
			for (smi = 0; smi < output.length; smi++)
				if (output[smi] == 1) break;
			GetOutput()[smi]--;
		}
		int x = V.length - 1;
		int i, j;
		for (i = 0; i < E[x].length; i++)
			E[x][i] = (output[i] - V[x][i]) * AF[x - 1].ff(V[x][i]);
		double sum;
		for (x--; x > 0; x--) {
			for (i = 0; i < E[x].length; i++) {
				sum = 0;
				for (j = 0; j < E[x + 1].length; j++) {
					sum += W[x][i][j] * E[x + 1][j];
					W[x][i][j] += Rate * E[x + 1][j] * V[x][i];
				}
				E[x][i] = sum * AF[x - 1].ff(V[x][i]);
			}
			for (j = 0; j < E[x + 1].length; j++)
				W[x][i][j] += Rate * E[x + 1][j];
		}
		for (i = 0; i < E[x].length; i++)
			for (j = 0; j < E[x + 1].length; j++)
				W[x][i][j] += Rate * E[x + 1][j] * V[x][i];
		for (j = 0; j < E[x + 1].length; j++)
			W[x][i][j] += Rate * E[x + 1][j];
		if (SM) GetOutput()[smi]++;
	}

	public void OutputWeight() {
		for (int i = 0, j, k; i < W.length; i++) {
			for (j = 0; j < W[i].length; j++) {
				for (k = 0; k < W[i][j].length; k++) System.out.printf("%.8f ", W[i][j][k]);
				System.out.println();
			}
			System.out.println("=================");
		}
	}

	public void OutputValues() {
		for (int i = 0, j; i < V.length; i++) {
			for (j = 0; j < V[i].length; j++) System.out.printf("%.8f ", V[i][j]);
			System.out.println();
		}
	}

	public double Cost(double[] target) {
		if (target.length != GetOutputSize())
			throw new IllegalArgumentException("Expected Output Size: " + GetOutputSize());
		double ret = 0;
		int x = V.length - 1;
		for (int i = 0; i < V[x].length; i++)
			ret += (target[i] - V[x][i]) * (target[i] - V[x][i]);
		return ret / V[x].length;
	}

	public double[][][] CreateEmptyWeight() {
		double[][][] ret = new double[W.length][][];
		for (int i = 0; i < W.length; i++)
			ret[i] = new double[W[i].length][W[i][0].length];
		return ret;
	}

	public void CopyWeight(double[][][] d) {
		for (int i = 0, j; i < W.length; i++)
			for (j = 0; j < W[i].length; j++) System.arraycopy(W[i][j], 0, d[i][j], 0, W[i][j].length);
	}

	public void SetWeight(double[][][] d) {
		for (int i = 0, j; i < W.length; i++)
			for (j = 0; j < W[i].length; j++) System.arraycopy(d[i][j], 0, W[i][j], 0, d[i][j].length);
	}

	public double MaxValue() {
		return GetOutput()[MaxIndex()];
	}

	public int MaxIndex() {
		double[] output=GetOutput();
		double max=output[0];
		int maxp=0;
		for (int i=1;i < output.length;i++)
			if (output[i] > max) {
				max = output[i];
				maxp = i;
			}
		return maxp;
	}

	public static void main(String[] args) {
		WritingTest();
	}

	private static void WritingTest() {
		final String P="/sdcard/AppProjects/HandWriting.nn";
		double[][] tin=new double[][]{{
				1,1,1,
				1,0,1,
				1,0,1,
				1,0,1,
				1,1,1
			},{
				0,0,1,
				0,0,1,
				0,0,1,
				0,0,1,
				0,0,1
			},{
				1,1,1,
				0,0,1,
				1,1,1,
				1,0,0,
				1,1,1
			},{
				1,1,1,
				0,0,1,
				1,1,1,
				0,0,1,
				1,1,1
			},{
				1,0,1,
				1,0,1,
				1,1,1,
				0,0,1,
				0,0,1
			},{
				1,1,1,
				1,0,0,
				1,1,1,
				0,0,1,
				1,1,1
			},{
				1,1,1,
				1,0,0,
				1,1,1,
				1,0,1,
				1,1,1
			},{
				1,1,1,
				0,0,1,
				0,0,1,
				0,0,1,
				0,0,1
			},{
				1,1,1,
				1,0,1,
				1,1,1,
				1,0,1,
				1,1,1
			},{
				1,1,1,
				1,0,1,
				1,1,1,
				0,0,1,
				1,1,1
			}};
		double[] tout=new double[tin.length];

		/*NeuralNetwork net = new NeuralNetwork(0.15, 15, 8, 8, tin.length);
		 net.RandomizeWeight();
		 net.AF = ActivationFunction.None;
		 Random r = new Random();
		 final int S = 150000;
		 double[][][] w = net.CreateEmptyWeight();
		 double costa, costb;
		 System.out.println("Training...");
		 int t;
		 for (int i = 0; i < S; i++) {
		 t=r.nextInt(tin.length);
		 tout[t] = 1;
		 net.Input(tin[t]);
		 net.CopyWeight(w);
		 costa = net.Cost(tout);
		 net.Fit(tout);
		 net.Input(tin[t]);
		 costb = net.Cost(tout);
		 if (costb <= costa) net.Rate *= (1 + costb);
		 else {
		 net.SetWeight(w);
		 net.Rate /= 2;
		 }
		 tout[t]=0;
		 }
		 net.OutputWeight();
		 try {
		 FileOutputStream out=new FileOutputStream(P);
		 DataOutputStream dos=new DataOutputStream(out);
		 net.WriteTo(dos);
		 dos.close();
		 out.close();
		 } catch (Throwable th) {
		 th.printStackTrace();
		 }*/

		NeuralNetwork net=null;
		try {
			FileInputStream in=new FileInputStream(P);
			DataInputStream din=new DataInputStream(in);
			net = new NeuralNetwork(din);
			din.close();
			in.close();
		} catch (Throwable t) {
			t.printStackTrace();
		}
		net.OutputWeight();
		double[] out=net.GetOutput();
		int maxp;
		double max;
		for (int i=0,j;i < tin.length;i++) {
			net.Input(tin[i]);
			max = out[0];
			maxp = 0;
			for (j = 1;j < tin.length;j++)
				if (out[j] > max) {
					max = out[j];
					maxp = j;
				}
			System.out.println(maxp);
		}
		System.out.println("==============");
		net.Input(new double[]{
					  1,1,1,
					  0,0,1,
					  0,1,0,
					  1,0,0,
					  1,1,1
				  });
		max = out[0];
		maxp = 0;
		for (int j=1;j < tin.length;j++)
			if (out[j] > max) {
				max = out[j];
				maxp = j;
			}
		System.out.println(maxp);
		System.out.println(Arrays.toString(out));
	}

	private static void FunctionTest() {
		final String P="/sdcard/AppProjects/Test.nn";
		double[] tin = new double[2];
		double[] tout = new double[1];

		/*NeuralNetwork net = new NeuralNetwork(0.15, 2, 1);
		 net.RandomizeWeight();
		 net.AF = ActivationFunction.None;
		 Random r = new Random();
		 final int S = 3000000;
		 double[][][] w = net.CreateEmptyWeight();
		 double costa, costb;
		 System.out.println("Training...");
		 for (int i = 0; i < S; i++) {
		 tin[0] = r.nextInt(100);
		 tin[1] = r.nextInt(100);
		 tout[0] = (tin[0] + tin[1]) / 2;
		 net.Input(tin);
		 net.CopyWeight(w);
		 costa = net.Cost(tout);
		 net.Fit(tout);
		 net.Input(tin);
		 costb = net.Cost(tout);
		 if (costb <= costa) net.Rate *= (1 + costb);
		 else {
		 net.SetWeight(w);
		 net.Rate /= 2;
		 }
		 }
		 net.OutputWeight();
		 try {
		 FileOutputStream out=new FileOutputStream(P);
		 DataOutputStream dos=new DataOutputStream(out);
		 net.WriteTo(dos);
		 dos.close();
		 out.close();
		 } catch (Throwable t) {
		 t.printStackTrace();
		 }*/

		NeuralNetwork net=null;
		try {
			FileInputStream in=new FileInputStream(P);
			DataInputStream din=new DataInputStream(in);
			net = new NeuralNetwork(din);
			din.close();
			in.close();
		} catch (Throwable t) {
			t.printStackTrace();
		}
		net.OutputWeight();
		Scanner scan = new Scanner(System.in);
		while (true) {
			tin[0] = scan.nextInt();
			tin[1] = scan.nextInt();
			tout[0] = (tin[0] + tin[1]) / 2;
			net.Input(tin);
			net.Fit(tout);
			System.out.printf("f(%d,%d)=%.4f, Cost: %.4f\n", (int) tin[0], (int) tin[1], net.GetOutput()[0], net.Cost(tout));
		}
	}

	private static void NumberTest() {
		NeuralNetwork net = new NeuralNetwork(0.15, new int[]{32, 2});
		net.RandomizeWeight();
		double[] tin = new double[32];
		double[] tout = new double[2];
		final int S = 200000;
		Random r = new Random();
		int t;
		System.out.println("Training...");
		for (int i = 0, j; i < S; i++) {
			t = r.nextInt();
			tout[0] = t >= 0 ? 1 : 0;
			tout[1] = ((t & 1) == 0) ? 1 : 0;
			for (j = 0; j < 32; j++) {
				tin[j] = t & 1;
				t >>>= 1;
			}
			net.Input(tin);
			net.Fit(tout);
		}
		System.out.println("Trained");
		net.OutputWeight();
		Scanner scan = new Scanner(System.in);
		int ori;
		while (true) {
			ori = t = scan.nextInt();
			tout[0] = t >= 0 ? 1 : 0;
			tout[1] = ((t & 1) == 0) ? 1 : 0;
			for (int j = 0; j < 32; j++) {
				tin[j] = t & 1;
				t >>>= 1;
			}
			net.Input(tin);
			net.Fit(tout);
			System.out.printf("%d >= 0: %.2f%%, %d is Oven: %.2f%%, Cost: %.2f\n", ori, net.GetOutput()[0] * 100, ori, net.GetOutput()[1] * 100, net.Cost(tout));
		}
	}
}
